package no.ntnu.item.arctis.runtime;

import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;

public class Scheduler implements Runnable {

	static final boolean debug = false;

	private boolean keepRunning;
	private Runtime register;
	private final TimerQueue timers = new TimerQueue();
	private final Vector uninitatedStateMachines = new Vector();
	private final Vector queues = new Vector();
	private int index = 0;

	public Scheduler(Runtime register) {
		this.register = register; 
	}

	public void add(IStateMachine queue) {
		uninitatedStateMachines.addElement(queue);
	}

	private void deregisterStateMachine(IStateMachine sm) {
		debug("Deregistering state machine: " + sm.getSessionId(), "deregisterStateMachine");
		register.terminateStateMachine(sm);		
		queues.removeElement(sm);
		uninitatedStateMachines.removeElement(sm);
		timers.removeStateMachine(sm);
		//sm.deregisterAllAliases();
		
		// TODO also remove all "child" state machines
		// for now, we do NOT destroy the scheduler, since we could wait for 
		// session creating signals from outside.
//		if (queues.isEmpty()) {
//			debug("No more state machines to execute, closing scheduler!", "deregisterStateMachine");
//			destroy();
//		}
	}

	public void run() {
		/*
		 * This method implements a prioritized round robin scheduling scheme
		 * with the following prioritization: (1) Timers, (2) Uninitiated and
		 * registered state machines, (3) Normal messages
		 */
		keepRunning = true;
		debug("Scheduler starts running.", "run");

		while (keepRunning) {

			// expired timers
			while (timers.hasExpiredTimer()) {
				TimerQueue.Timer timer = timers.consume();
				try {
					
					long time = System.currentTimeMillis();
					int prestate = timer.stateMachine._state;
					int result = timer.stateMachine.fireTimer(timer.id);
					Logger.getLogger().transition("timeout " + timer.id, prestate + " --> " + timer.stateMachine._state, 
							System.currentTimeMillis() - time + " ms", timer.stateMachine);
					if(result==IStateMachine.TERMINATE_MACHINE) {
						deregisterStateMachine(timer.stateMachine);
					} else if (result==IStateMachine.BLOCKING_CHOICE) {
						Logger.getLogger().blockingChoice();
					}
				} catch (Throwable t) {
					// TODO make state readable from state machine
					Logger.getLogger().applicationErrorDuringFireTimer(timer.stateMachine, "", timer.id, t);
				}

			}

			// state machines to initialize
			// TODO use better loop with for
			while (!uninitatedStateMachines.isEmpty()) {
				debug("Initiating state machine(s).", "run");
				IStateMachine sm = (IStateMachine) uninitatedStateMachines.elementAt(0);
				uninitatedStateMachines.removeElementAt(0);

				try {
					long time = System.currentTimeMillis();
					int result = sm.fireInitial();
					Logger.getLogger().transition("<init>", "<initial> --> " + sm._state, 
							System.currentTimeMillis() - time + " ms", sm);
					if(result==IStateMachine.TERMINATE_MACHINE) {
						deregisterStateMachine(sm);
					} else if (result==IStateMachine.BLOCKING_CHOICE) {
						Logger.getLogger().blockingChoice();
					} else {
						queues.addElement(sm);
					}
				} catch (Throwable t) {
					t.printStackTrace();
					Logger.getLogger().applicationErrorDuringFireInitial(sm, t);
				}
			}

			// next state machine with 
			if (queues.size()>0) {
				if(index > queues.size()-1) index = 0;
				IStateMachine nextSm = (IStateMachine) queues.elementAt(index);
				if(nextSm==null) break;
				++ index;
				boolean consumed = false;
				boolean discarded = false;
				Message msg = nextSm.getInputQueue().peakAtFirstNonMarkedMessage();
				while (!consumed && !discarded && (msg != null)) {

					int result = IStateMachine.EXCEPTION;

					try {
						long time = System.currentTimeMillis();
						int prestate = nextSm._state;
						result = nextSm.fire(msg.sender, msg.receiver, msg.signalID, msg.content);
						Logger.getLogger().transition(msg.signalID, prestate + " --> " + nextSm._state, 
								System.currentTimeMillis() - time + " ms", nextSm);
					} catch (Throwable t) {
						Logger.getLogger().applicationErrorDuringFire(nextSm, "" + nextSm.getClass().getSimpleName(), msg.signalID, t);
					}

					if (result == IStateMachine.CONSUME_SIGNAL) {
						nextSm.getInputQueue().removeMessage(msg);
						nextSm.getInputQueue().unmarkAllMessages();
						debug("consumed", "run");
						consumed = true;
					} else if (result == IStateMachine.DISCARD_SIGNAL) {
						nextSm.getInputQueue().removeMessage(msg);
						discarded = true;
						Logger.getLogger().discardedSignal(msg.signalID, msg.sender, msg.receiver, nextSm.getClass().getSimpleName(), "" + nextSm._state);
					} else if (result == IStateMachine.TRIGGER_UNKNOWN) {
						nextSm.getInputQueue().removeMessage(msg);
						discarded = true;
						Logger.getLogger().unknownTrigger(msg.signalID, msg.sender, msg.receiver, nextSm.getClass().getSimpleName());
					} else if (result == IStateMachine.BLOCKING_CHOICE) {
						nextSm.getInputQueue().removeMessage(msg);
						consumed = true;
						Logger.getLogger().blockingChoice();
					} else if (result == IStateMachine.SAVE_SIGNAL) {
						debug("marked as saved", "run");
						msg.markAsSaved(true);
						msg = nextSm.getInputQueue().peakAtFirstNonMarkedMessage();
					} else  if (result == IStateMachine.TERMINATE_MACHINE) {
						nextSm.getInputQueue().removeMessage(msg);
						nextSm.getInputQueue().unmarkAllMessages();
						deregisterStateMachine(nextSm);
						consumed = true;
					} else {
						// an exception happened. signal is marked as consumed
						// to not try again
						nextSm.getInputQueue().removeMessage(msg);
						nextSm.getInputQueue().unmarkAllMessages();
						consumed = true;
					}
				}
			} else {
				debug("Warning: Scheduler is running, but could not find a state machine to execute!", "run");
			}

			if (uninitatedStateMachines.isEmpty() && allInputQueuesAreEmpty() && keepRunning) {
				/*
				 * Wait untill next timer expires OR an incoming message
				 * notifies the object, see deliver().
				 */
				debug("No initiated state machines and all input queues are empty, will wait.", "run");
				try {
					synchronized (this) {
						if (timers.isEmpty()) {
							debug("Waiting for external message.", "run");
							wait();
						} else {
							long wait = timers.getTimeToNextExpiration();
							if (wait > 10) {
								debug("Waiting for external message or timer expiration in " + wait + " ms.", "run");
								wait(wait);
							}
						}
					}
				} catch (Throwable e) {
					// Ignore.
					e.printStackTrace();
				}
			} 
		}
	}
	
	public String createFreshSessionID() {
		return register.getFreshSessionId();
	}
	
	public void sendToSession(IStateMachine sender, String sessionID, String signalID, Object data) {
		sendToSession(sender, sessionID, signalID, data, null);
	}

	public void sendToSession(IStateMachine sender, String sessionID, String signalID, Object data, String alias) {
//		if(sessionID==null) {
//			Logger.getLogger().sessionSelectionFailed(sender.getClass().getSimpleName(), signalID, sessionID);
//		} else {
			register.deliverInternalMessage(sender, sessionID, signalID, data, alias);
//		}
	}
	
	public void sendToSessions(IStateMachine sender, Collection sessionIDs, String signalID, Object data) {
		Iterator i = sessionIDs.iterator();
		while(i.hasNext()) {
			register.deliverInternalMessage(sender, (String) i.next(), signalID, data, null);
		}
	}
	
	public void sendToMainMachine(IStateMachine sender, String signalID, Object data) {
		register.deliverInternalMessage(sender, null, signalID, data, null);
	}
	
	public void startTimer(int delay, String timerId, Object stateMachine) {
		timers.startTimer(delay, timerId, (IStateMachine) stateMachine);
		debug("Starting timer with ID: " + timerId, "startTimer");
	}

	public void stopTimer(String timerId, Object stm) {
		timers.stopTimer(timerId, (IStateMachine) stm);
	}

	public void restartTimer(int delay, String timerId, Object stm) {
		timers.restartTimer(delay, timerId, (IStateMachine) stm);
	}
	
	public void sendToParent(IStateMachine sender, String signalID, Object data) {
		register.deliverInternalMessage(sender, sender.parentSessionID, signalID, data, null);
	}
	
	public void sendToParent(IStateMachine sender, String signalID) {
		register.deliverInternalMessage(sender, sender.parentSessionID, signalID, null, null);
	}

	public void destroy() {
		keepRunning = false;
	}

	private boolean allInputQueuesAreEmpty() {
		for (int i = 0; i < queues.size(); i++) {
			IStateMachine queue = (IStateMachine) queues.elementAt(i);
			if(!queue.getInputQueue().isEmpty()) {
				return false;
			}
		}
		return true;
	}

    void awakenScheduler() {
        synchronized (this) {
            notifyAll();
        }
    }


	protected void debug(String text, String method) {
		if (debug)
			System.out.println(this.getClass().getName() + "." + method + ":\n\t" + text);
	}


class TimerQueue {
	
	private class Timer {
		
		String id;
		int delay;
		long time;
		IStateMachine stateMachine;

		Timer(int delay, String id, IStateMachine stateMachine) {
			this.delay = delay;
			this.time = System.currentTimeMillis() + delay;
			this.id = id;
			this.stateMachine = stateMachine;
		}

		void reset() {
			this.time = System.currentTimeMillis() + delay;
		}

	}
	
	private final Vector timers = new Vector();

	public TimerQueue() {
	}

	public void removeStateMachine(IStateMachine stm) {
		Vector timerToDelete = null;

		for (int i = 0; i < timers.size(); i++) {
			Timer t = (Timer) timers.elementAt(i);
			if (t.stateMachine == stm) {
				if (timerToDelete == null) {
					timerToDelete = new Vector();
				}
				timerToDelete.addElement(t);
			}
		}


		if (timerToDelete != null) {
			for (int i = 0; i < timerToDelete.size(); i++) {
				Timer t = (Timer) timerToDelete.elementAt(i);
				timers.removeElement(t);
			}
		}
	}

	public Timer consume() {
		// assert timers.size()>0;
		Timer timer = (Timer) timers.elementAt(0);
		timers.removeElementAt(0);
		return timer;
	}

	public boolean hasExpiredTimer() {
		return (!timers.isEmpty()) && ((Timer) timers.elementAt(0)).time <= System.currentTimeMillis();
	}

	public boolean isEmpty() {
		return timers.isEmpty();
	}
	
	public long getTimeToNextExpiration() {
			long duration = ((Timer) timers.elementAt(0)).time - System.currentTimeMillis();
		return duration>0 ? duration : 0;
	}
	
	public void startTimer(int delay, String timerId, IStateMachine stateMachine) {
		TimerQueue.Timer timer = new TimerQueue.Timer(delay, timerId, (IStateMachine) stateMachine);
		insertTimerInSortedVector(timer);
	}

	public void stopTimer(String timerId, IStateMachine stm) {
			Timer timerToRemove = null;
			for (int i = 0; i < timers.size(); i++) {
				Timer t = (Timer) timers.elementAt(i);
			if (t.id.equals(timerId) && (t.stateMachine == stm)) {
					timerToRemove = t;
					break;
			}
		}
			if (timerToRemove != null) {
				timers.removeElement(timerToRemove);
			}
	}

	public void restartTimer(int delay, String timerId, IStateMachine stm) {
			Timer timerToRestart = null;
			for (int i = 0; i < timers.size(); i++) {
				Timer t = (Timer) timers.elementAt(i);
				if (t.id.equals(timerId) && (t.stateMachine == stm)) {
					timerToRestart = t;
					break;
				}
			}
			if (timerToRestart != null) {
				timers.removeElement(timerToRestart);
				timerToRestart.reset();
				insertTimerInSortedVector(timerToRestart);
			} else {
				startTimer(delay, timerId, stm);
			}
	}
			
	
	private void insertTimerInSortedVector(TimerQueue.Timer timer) {
		if (timers.size() == 0) {
				timers.addElement(timer);
		} else {
			int i = 0;
			boolean endReached = false;
				while (((Timer) timers.elementAt(i)).time < timer.time) {
				if (i + 1 == timers.size()) {
					endReached = true;
					break;
				} else {
					i++;
				}
			}
			if (!endReached) {
					timers.insertElementAt(timer, i);
			} else {
					timers.addElement(timer);
			}
		}
	}
}

static class Message {

	public final String signalID;
	public final String sender;
	public final String receiver;
	public final Object content;
	public boolean marked;

	public Message(String sender, String receiver, String signalID, Object content) {
		this.sender = sender;
		this.receiver = receiver;
		this.content = content;
		this.signalID = signalID;
		marked = false;
	}

	public String toString() {
		String n = System.getProperty("line.separator");
		StringBuffer s = new StringBuffer();
		s.append(n + "SignalID: " + signalID);
		s.append(n + "Sender: " + sender);
		s.append(n + "Receiver: " + receiver);
		s.append(n + "Content :" + content);
		s.append(n + "Marked: " + marked);
		return s.toString();
	}

	public boolean isMarked() {
		return marked;
	}

	public void markAsSaved(boolean b) {
		marked = b;
	}
}

static class InputQueue {
	
	private final Vector externalMessages;
	private final Vector internalMessages;
	private IStateMachine machine;
	
	public InputQueue(IStateMachine machine) {
		this.machine = machine;
			externalMessages = new Vector();
			internalMessages = new Vector();
	}
	
	public IStateMachine getStateMachine() {
		return machine;
	}

	public boolean isEmpty() {
		return (externalMessages.isEmpty() && internalMessages.isEmpty());
	}
	
	public synchronized void addExternalMessage(String sender, String sessionID, String signalID, Object data) {
			externalMessages.addElement(new Message(sender, sessionID, signalID, data));
	}

	public void addInternalMessage(String sender, String sessionID, String signalID, Object data) {
			internalMessages.addElement(new Message(sender, sessionID, signalID, data));
	}

	public synchronized void addExternalMessage(Message msg) {
			externalMessages.addElement(msg);
	}

	public void addInternalMessage(Message msg) {
			internalMessages.addElement(msg);
	}

	/**
	 * @return the first non-marked message or null if there are no such
	 *         messages
	 */
	public Message peakAtFirstNonMarkedMessage() {
			for (int i = 0; i < internalMessages.size(); i++) {
				Message nextMsg = (Message) internalMessages.elementAt(i);
			if (!nextMsg.isMarked()) {
				return nextMsg;
			}
		}
			for (int i = 0; i < externalMessages.size(); i++) {
				Message nextMsg = (Message) externalMessages.elementAt(i);
			if (!nextMsg.isMarked()) {
				return nextMsg;
			}
		}
		return null;
	}

	public void unmarkAllMessages() {
		for (int i = 0; i < internalMessages.size(); i++) {
			Message nextMsg = (Message) internalMessages.elementAt(i);
			nextMsg.markAsSaved(false);
		}
		for (int i = 0; i < externalMessages.size(); i++) {
			Message nextMsg = (Message) externalMessages.elementAt(i);
			nextMsg.markAsSaved(false);
		}
	}

	public void removeMessage(Message msg) {
		if (internalMessages.contains(msg)) {
				internalMessages.removeElement(msg);
		} else if (externalMessages.contains(msg)) {
				externalMessages.removeElement(msg);
		} else {
			debug("Warning: Tried to remove message from an InputQueue that did not contain it!", "removeMessage");
		}
	}
	
	public void removeInternalSignal(String signalId) {
		for(int i=0; i<internalMessages.size(); i++) {
			Message nextMsg = (Message) internalMessages.elementAt(i);
			if(nextMsg.signalID.equals(signalId)) {
				internalMessages.removeElementAt(i);
				--i;
			}
		}
	}

	protected void debug(String text, String method) {
		if (Scheduler.debug)
			System.out.println(this.getClass().getName() + "." + method + ":\n\t" + text);
	}

	public void print() {
			// for(Message m: externalMessages) {
			// System.out.println("M" + m.signalID);
			// }
			// for(Message m: internalMessages) {
			// System.out.println("M" + m.signalID);
			// }
	}
}

}